/*
*ICompressor.java 
*Santiago Perez
*Galen Marisco
*
*This class handles all image compression/ color transformations and output
*
*/

import java.awt.image.BufferedImage;
import java.awt.image.PixelGrabber;
import java.io.*;
import java.lang.Exception.*;

public class ICompressor {
	//Global Variables
	int subArr[][];//the subsampled matrix of size m/2 *n/2 but it should be multiple of 8*8 so it may have to be padded
	
	int size = 8;//magic number, it is the size of the pixel blocks
	
	double[][] cosMatrix  = new double[size][size];//used for the DCT
	double[][] TcosMatrix = new double[size][size];//used for the DCT
	
	int[][] lumaQ   = new int[size][size];//the luminance  Quantization Matrix
	int[][] chromaQ = new int[size][size];//the chrominance Quantization Matrix
	MedianCutter med;
	int newWidth;
	int newHeight;
	int[][] blueArray=null;
	int[][] greenArray=null;
	int[][] redArray=null;
	int[][] yArray=null;
	int stepSize;
	int bitdepth;
	int split[] = new int[7];
	double[][][] rescaledY=null;
	File outputFile;
	//shape detection
	double[][] upperBlock, lowerBlock, leftBlock, rightBlock;
	
	
	
	ICompressor(){
		
		//make the 8x8 Matrix with the cosine Values 
		// and also make the Transpose of it so they are ready 
		startCosMatrixes();// start the cosine and the inverse cosine  Matrix
		
		startLumaQM();//start the Luma Quantization Matrix for Y
		outputFile = null;
		med = new MedianCutter();
	}//end constructor
	
	
	/********grabbing image/image transform*********/
	public int[][] converttoYArray(int[][] redArray, int[][] greenArray, int[][] blueArray,int width,int height) {
		int result[][] = new int[width][height];
		for(int x=0;x<width;x++){
			for(int y=0;y<height;y++){
				result[x][y]=(int)((0.299 * (float)redArray[x][y]) + (0.587 * (float)greenArray[x][y]) + (0.114 * (float)blueArray[x][y]));
			}//end for
		}//end for 
		return result; 
	}//end converttoYUVArray
	
	public void processImage(BufferedImage inImage, int depth, boolean shapeDetection){
		int w = inImage.getWidth(null);
		int h = inImage.getHeight(null);
		bitdepth=depth;
		redArray   = getArray(inImage,w,h,"red");
		greenArray = getArray(inImage,w,h,"green");
		blueArray  = getArray(inImage,w,h,"blue");
		yArray=converttoYArray(redArray,greenArray,blueArray,newWidth,newHeight);
		/*here is the more complicated group of for loops, read the whole subsampled image
		* in block of 8x8 and then to each one apply DCT quantization in order to compress.
		* And then start to do the reverse in order to recover the image.The following steps have to
		* be done for each YUV channel */
		
		//this variables will hold the 8x8 blocks of the subsampled UV and the original Y channel
		int[][]preDCTedYblock = new int[8][8];
		
		//to the previous blocks DCT will be applied
		int[][]DCTedYblock = new int[8][8]; 
		
		//to the previous block Quantization will be applied
		int[][]QedYblock = new int[8][8]; 
		
		//the result of a newWidth by newHeight fully compressed Channel
		int[][]compY = new int[newWidth][newHeight];
		
		stepSize = getStepSize(newWidth, newHeight);
		
		for(int x = 0; x < newWidth / 8; x++){
			for(int y = 0; y < newHeight / 8; y++){
				for(int i = 0; i < 8; i++){
					for(int j = 0; j < 8; j++){
						preDCTedYblock[i][j] = yArray[x*8+i][y*8+j];
					}//end for
				}//end of reading a block
				
				//DCT to each block
				DCTedYblock =performDCT(preDCTedYblock);
				
				//Quantize each block
				if(stepSize != 1){
					QedYblock = quantize(DCTedYblock, "y");
				}else{
					QedYblock = DCTedYblock;
				}//end if/else block
				
				//build a fully compressed channels from the 8x8 blocks
				for(int i=0; i<8;i++){
					for(int j=0;j<8;j++){
						compY[x*8+i][y*8+j]=QedYblock[i][j];
					}//end for
				}//end for												
			}//end for
		}//end for
		
		/*The following steps have to be done for each YUV channel  in order to uncompress the image  and restore it
		* its kind of the code above but things are done in reverse order starting with compY compU compV*/
		
		//just like before the 8x8 predequantized block 
		int[][] preDQYblock = new int[8][8];
		
		// to the previous block dequantization will be applied
		int[][] deqYblock = new int[8][8];
		
		// to the previous block IDCT will be applied
		int[][] iDCTedYblock = new int[8][8];
		
		//the result of a newWidth by newHeight fully UNcompressed Channel
		int[][] uncompY = new int[newWidth][newHeight];
		
		for(int x = 0; x < newWidth / 8; x++){
			for(int y = 0; y < newHeight / 8; y++){
				for(int i = 0; i < 8; i++){
					for(int j = 0 ;j < 8; j++){
						preDQYblock[i][j] = compY[x*8 +i][y*8+j];
					}//end for
				}//end of reading a block
				//Dequantize each block
				if(stepSize != 1){
					deqYblock =dequantize(preDQYblock, "y");
				}else{
					deqYblock = preDQYblock;
				}//end if/else block
				
				//Inverse DCT each block
				iDCTedYblock = performIDCT(deqYblock);
				
				//build a fully UNcompressed channels from the 8x8 blocks
				for(int i = 0; i < 8; i++){
					for(int j = 0; j < 8; j++){
						uncompY[x*8+i][y*8+j] = iDCTedYblock[i][j];
					}//end for
				}//end for												
			}//end for
		}//end Uncompressing
		//int maxYval = getMaxYval(uncompY, newWidth, newHeight);
		//int minYval = getMinYval(uncompY, newWidth, newHeight);
		
		rescaledY = getMatrixWithShapeRecognition(uncompY, newHeight, newWidth, stepSize);
		printBitmapToConsole(rescaledY, newHeight, newWidth, stepSize, depth, shapeDetection);
		
	}//end processImage
	
	
	
	/*******Compression******/

	public void startCosMatrixes(){
		/*Initialize the cosineMatrix when i=0 *1/2sqrt8 for all the first row */
		for(int j = 0; j < size; j++){
			cosMatrix[0][j] = 1.0/Math.sqrt((double)(size));
		/*Initialize the TcosineMatrix the transpose of cosMatrix */
			TcosMatrix[j][0] = cosMatrix[0][j];	
		}//end for
		/*Now fill out the remaining of cosMatrix and TcosMatrix*/
		for(int i = 1; i < size; i++){
			for(int j = 0; j < size; j++){
				cosMatrix[i][j]  = Math.sqrt(2.0/8.0) * Math.cos(((2.0 * (double)j + 1.0) * (double)i * Math.PI) / (2.0 * 8.0));
				TcosMatrix[j][i] = cosMatrix[i][j];		
			}//end for	
		}//end for	
	}//end startCosMatrixes	
	
	public int[][] performDCT(int[][] inArray){
		int[][] result = new int[size][size];
		double[][] tempArr = new double[size][size];
		int i,j,k;
		double val;
		/*matrix multiplication is not commutative but 
		 * it is associative. Multiply cosMatrix chroma TcosMatrix
		 * like T*f(ij)*T'                                */
		
		//tempArry= f(i,j)*T'
		
		for (i = 0; i < size; i++){//for each row
            for (j = 0; j < size; j++){//for each column
                tempArr[i][j] = 0.0;// so we can do the actual matrix multiplication
                for (k = 0; k < size; k++){	
                	inArray[i][k] = inArray[i][k] - 128;            	
                    tempArr[i][j] += ((int)(inArray[i][k]) * TcosMatrix[k][j]);
                }//end for
            }//end for
        }//end first 1DCT
		
		//result= cosMatrix*tempArray
		for (i = 0; i < size; i++){ 
	            for (j = 0; j < size; j++){
	                val = 0.0;//add up all the values here for the corresponding coordinate of  F(uv)
	                for (k = 0; k < size; k++){
	                    val += (cosMatrix[i][k] * tempArr[k][j]);
	                }//end for
	                result[i][j]=(int)Math.round(val);
	            }//end for 
	    }//end for	
		return result;	
	}//end DCT
	
	public void startLumaQM(){
		
		lumaQ = new int[8][8];
		int intermed_l[][]  =    {{ 16,  11,  10,  16,  24,  40,  51,  999},
								  { 12,  12,  14,  19,  26,  58,  60,  999},
								  { 14,  13,  16,  24,  40,  57,  69,  999},
								  { 14,  17,  22,  29,  51,  87,  80,  999},
								  { 18,  22,  37,  56,  68, 109, 103,  999},
								  { 24,  35,  55,  64,  81, 104, 113,  999},
								  { 49,  64,  78,  87, 103, 121, 999,  999},
								  {999, 999, 999, 999, 999, 999, 999,  999}};
		lumaQ = intermed_l;
		
	}//end startLumaQM
		
	public int[][] quantize(int[][] channel, String type){
		//Have to make a quantization in 8x8 channels, and use the corresponding Quatization matrix for Luma or Chrominace
		int[][] result = new int[size][size];
			//perform quantization for the luma component Y
		for(int i=0;i<size;i++){
			for(int j=0;j<size;j++){
				result[i][j]=(int)Math.round((float)(channel[i][j])/(lumaQ[i][j]));//round and cast to int the result of each ij component
			}//end for
		}//end for
		return result;//either return chroma U,V after quantization or the luma Y one
	} //end quantizise
	
	public int[][] performIDCT(int[][] inArray){
		int[][] result= new int[size][size];
		double[][] tempArr= new double[size][size];
		double val;
		int i, j ,k;
		//f(i,j) = T^T * F(u,v) * T.  Note, the places for T and T^T (T-transpose) are switched.
		for (i = 0; i < size; i++){
            for(j = 0; j < size; j++){
                tempArr[i][j] = 0.0;//tempArray  =F(u,v) * T
                for (k = 0; k < size; k++){	
                    tempArr[i][j] += inArray[i][k] * cosMatrix[k][j];
                }//end for
            }//end for
        }//end for
		//result=T^T*TempArray
		for(i=0; i<size; i++){
			for (j=0; j<size; j++){
				val = 0.0;
				for(k = 0; k < size; k++){
					val += TcosMatrix[i][k] * tempArr[k][j]; 
				}//end for
				result[i][j] = (int) Math.round(val); 
				result[i][j] = result[i][j] + 128;
				if(result[i][j]>=255)
					result[i][j] = 255;
				if(result[i][j]<=0)
					result[i][j] = 0;
			}//end for
		}//end for
		return result;
	}//end performIDCT
	
	public int[][] dequantize(int[][]channel, String type){
		//Have to make a quantization in 8x8 channels, and use the corresponding Quatization matrix for Luma or Chrominace
		int[][] result= new int[size][size];
		//perform quantization for the luma component Y
		for(int i=0;i<size;i++){
			for(int j=0;j<size;j++){
				result[i][j]=(int)Math.round((float)(channel[i][j])*(lumaQ[i][j]));//round and cast to int the result of each ij component
			}//end for
		}//end for
		return result;//either return chroma U,V after quantization or the luma Y one
	}//end dequantize
	
	public int getMaxYval(int yMatrix[][], int width, int height){
		int result = 0;
		for(int x = 0; x < width; x++){
			for(int y = 0; y < height; y++){
				if(yMatrix[x][y] > result)
					result = yMatrix[x][y];
			}//end for	
		}//end for
		return result;
	}//end getMaxYval
	
	public int getMinYval(int yMatrix[][], int width, int height) {
		int result = 255;
		for(int x = 0; x < width; x++){
			for(int y = 0; y < height; y++){
				if(yMatrix[x][y] < result)
					result = yMatrix[x][y];
			}//end for
		}//end for
		return result;
	}//end getMinYval
		
	/* the third dimension of the image array corresponds to the alignment of the pixels (in a given pixel block)
	 * with a left, right, top or bottom oriented block. 
	 * result[x][y][0] = the image
	 * result[x][y][1] = horizontal orientation(1 = left, 2 = right)
	 * result[x][y][2] = vertical orientation(3 = upper, 4 = lower)  */
	public double [][][] getMatrixWithShapeRecognition(int[][] yMatrix, int height, int width, int stepsize){
		
		double upperDiff, lowerDiff, leftDiff, rightDiff;
		
		double vertOrientation, horizOrientation;
		double[][] comparisonBlock = new double[stepsize][2*stepsize];
		generateComparisonBlocks(stepsize);
		double[][][] result = new double[width][height][3];
		
		//scan image
		for(int y = 0; y < height; y = y + stepsize*2){

			for(int x = 0; x < width; x = x + stepsize){
				
				//populate and compare each block
				for(int j = 0; j < 2*stepsize; j++){
					for(int i = 0; i< stepsize; i++){
						comparisonBlock[i][j] = (double)yMatrix[i+x][j+y];
					}
				}
				//upper or lower
				upperDiff = getMeanDifference(comparisonBlock,upperBlock,stepsize,stepsize*2);
				lowerDiff = getMeanDifference(comparisonBlock,lowerBlock,stepsize,stepsize*2);										
				leftDiff = getMeanDifference(comparisonBlock,leftBlock,stepsize,stepsize*2);				
				rightDiff = getMeanDifference(comparisonBlock,rightBlock,stepsize,stepsize*2);
				
				if(upperDiff>lowerDiff){
					//if upper difference is greater than lower difference, it is a "lower" block
					vertOrientation = 4;
				}else{
					vertOrientation = 3;
				}
				if(leftDiff>rightDiff){
					//if left difference is greater than right difference, it is a "right" block
					horizOrientation = 2;
				}else{
					horizOrientation = 0;
				}
				
				
				//THIS FIXED THE BUG
				for(int j = y; j < y + 2*stepsize; j++){
					for(int i = x; i< x+ stepsize; i++){
						result[i][j][0] = (double) yMatrix[i][j];
						result[i][j][1] = horizOrientation;
						result[i][j][2] = vertOrientation;
					}
				}
				
				//THIS  CAUSED THE BUG
				//result[x][y][0] = (double) yMatrix[x][y];
				//result[x][y][1] = horizOrientation;
				//result[x][y][2] = vertOrientation;
			}//end for


		}//end for
		/*for(int y = 0; y<height; y++){
			for(int x = 0; x<width; x++){
				System.out.print(result[x][y][2]);
			}
			System.out.println();
		}*/
		return result;
	}//end rescaleYMatrix
	
	public void generateComparisonBlocks(int stepsize){
		upperBlock = new double[stepsize][stepsize*2];
		lowerBlock = new double[stepsize][stepsize*2];
		leftBlock = new double[stepsize][stepsize*2];
		rightBlock = new double[stepsize][stepsize*2];
		for(int x = 0; x<stepsize; x++){
			for(int y = 0; y<2*stepsize; y++){
				upperBlock[x][y] = 0;
				if(y>=stepsize){
					upperBlock[x][y] = 0;
					lowerBlock[x][y] = 255;
				} else{
					upperBlock[x][y] = 255;
					lowerBlock[x][y] = 0;
				}
				if(x<=stepsize/2){
					leftBlock[x][y] = 0;
					rightBlock[x][y] = 255;
				} else{
					leftBlock[x][y] = 255;
					rightBlock[x][y] = 0;
				}
			}
		}
	}
		
	public double getMeanDifference(double[][] block1, double[][] block2, int width, int height){
		double difference[][] = new double[width][height];
		double sum = 0;
		for(int x = 0; x<width; x++){
			for(int y = 0; y<height; y++){
				difference[x][y] = Math.abs(block1[x][y] - block2[x][y]);
				sum += difference[x][y];
			}
		}
		sum = sum/(width*height);
		return sum;
	}
	
	//based on the size of the image, this method returns a multiplier for subsampling the image. 
	public int getStepSize(int width, int height){
		int area = width * height;
		if(area<=65536){
			return 1;			
		}else if(area<=640000){
			return 4;
		}else{
			return 8;
		}//end if/else block
	}//end getStepSize	
	
	private void printCharToConsole(double[] Yval, int[] splitmarkers, int bit, boolean shapeDetection){
		String output = convertDoubleToCharacter(Yval, splitmarkers, bit, shapeDetection);
		System.out.print(output);
	}//end printChartoFile
	
	public void printBitmapToConsole(double[][][] bitmap, int height, int width, int stepsize, int bit, boolean shapeDetection){
		if(bit == 1){
			split = med.getMedian1bit(bitmap, width, height);
			/*med.dumpDistribution();
			System.out.println("median: " + split[0]);*/
		}else if(bit == 2){
			split = med.getMedian2bit(bitmap, width, height);
			/*med.dumpDistribution();
			System.out.println("median: " + split[0]);
			System.out.println("median: " + split[1]);
			System.out.println("median: " + split[2]);*/
		}else{
			split = med.getMedian3bit(bitmap, width, height);
			/*med.dumpDistribution();
			System.out.println("median: " + split[0]);
			System.out.println("median: " + split[1]);
			System.out.println("median: " + split[2]);
			System.out.println("median: " + split[3]);
			System.out.println("median: " + split[4]);
			System.out.println("median: " + split[5]);
			System.out.println("median: " + split[6]);*/
		}
		for(int y = 0; y < height; y = y + stepsize*2){
			//System.out.print("<br>");
			for(int x = 0; x < width; x = x + stepsize){
				printCharToConsole(bitmap[x][y], split, bit, shapeDetection);
			}//end for
			System.out.println();
		}//end for
	}//end printBitmapToConsole	
	
	private File setOutputFile(){
		boolean correctInput=false;
		while(!correctInput){
			try{
				String filename = "picture";
				filename = ".\\"+filename+".txt";
				outputFile = new File(filename);
				FileInputStream makeSureFileExists = new FileInputStream(outputFile);
				makeSureFileExists = null;
				correctInput = true;
			}//end try
			catch(FileNotFoundException e){
				System.out.println("\nThe File you requested does not exist in Input directory\n");
			}//end catch
		}//end while
		return outputFile;
	}//end setOutputFile
	
	private String convertDoubleToCharacter(double value[], int[] split, int bit, boolean shapeDetection){
		if(bit == 1){
			if(value[0]<split[0]){
				return "#";
			}else{
				return " ";
			}
		}else if(bit == 2){
			if(value[0] < split[0]){
				return "#";	
			}else if(split[0] <= value[0] && value[0] < split[1]){
				return "*";
			}else if(split[1] <= value[0] && value[0] < split[2]) {
				if(value[2]==4 && shapeDetection){
					return "`";
				}else{
					return ".";	
				}
				
			}else{
				return " ";
			}//end if/else block
		}else{
			if(value[0] < split[0]){
				return "@";	
			}else if(split[0] <= value[0] && value[0] < split[1]){
				return "#";
			}else if(split[1] <= value[0] && value[0] < split[2]) {
				return "*";	
			}else if(split[2] <= value[0] && value[0] < split[3]){
				return "+";
			}else if(split[3] <= value[0] && value[0] < split[4]){
				return "=";	
			}else if(split[4] <= value[0] && value[0] < split[5]){
				return "-";
			}else if(split[5] <= value[0] && value[0] < split[6]) {
				return ":";	
			}else{
				return ".";
			}//end if/else block
		}//end if/else
		
		
		//" .:-=+*#%@"
		

	}//end convertDoubleToCharacter
	
	public void printBitMapToFile(double[][][] rescaledY,int newWidth,int newHeight,int step, int[]split, int bitdepth, boolean shapeDetection){
		String char_to_print;
		outputFile = setOutputFile();
		try{
			BufferedWriter output = null;
			output = new BufferedWriter(new FileWriter(outputFile));
			for(int y = 0; y < newHeight; y = y + step*2){
				for(int x = 0; x < newWidth; x = x + step){
					char_to_print = convertDoubleToCharacter(rescaledY[x][y], split, bitdepth, shapeDetection);
					output.write(char_to_print);
				}//end for
				output.newLine();
			}//end for
			output.close();
			System.out.println("Your file has been written");
		}catch(IOException f){
			System.out.println("Java IO exception while trying to write file");
		}//end catch

	}//end processQueueIntoFile
	
	public void printBitMapToFileHTML(double[][][] rescaledY,int newWidth,int newHeight,int step, int[]split, int bitdepth, boolean shapeDetection){
		String char_to_print;
		String tophtml="<html><head><style type=\"text/css\">body{white-space: pre;white-space: nowrap;}"+
						"</style></head><body><pre class=\"white\" style=\"font: 10px/5px monospace; text-align: center;\">";
		try{
            BufferedWriter writer = null;
            writer = new BufferedWriter( new FileWriter("picture.html"));
            writer.write(tophtml);writer.newLine();
            for(int y = 0; y < newHeight; y = y + step*2){
            	writer.write("<br>");
				for(int x = 0; x < newWidth; x = x + step){
					char_to_print = convertDoubleToCharacter(rescaledY[x][y], split, bitdepth, shapeDetection);
					writer.write(char_to_print);
				}//end for
				writer.newLine();
			}//end for
            writer.write("</body></html>");
			writer.close();
        }
        catch(Exception e){e.printStackTrace();}
		
		
		
		
	}//end prinbitmaptofile html
			
	//function that were originally from GUI
	public int[][] getArray(BufferedImage image, int width, int height, String color){
		newWidth  = width;
		newHeight = height;
		if(newWidth % 8 != 0)
			newWidth = (int) (Math.floor(newWidth/8.0) *8);
		if(newHeight % 8 != 0)
			newHeight = (int) (Math.floor(newHeight/8.0) *8);
		int values[] = new int[newWidth * newHeight];
    	PixelGrabber grabber = new PixelGrabber(image.getSource(), 0, 0, width, height, values, 0, width);
    	try{
    		grabber.getPixels();
    	}catch (Exception e){
    		e.printStackTrace();
    	}//end try/catch block

    	int result[][] = new int[newWidth][newHeight];
    	/*00 01 02 03
    	 *10 11 12 13 
    	 *20 21 22 23 
    	 *30 31 32 33
    	 * First index is for the rows, second index columns
    	 * Want to move first on the  columns, then on the rows
    	 * */
    	
    	if(color.equals("red")){
	    	for (int x = 0; x < newWidth; ++x){
	    		for (int y = 0; y < newHeight; ++y){
	    			int rgb=image.getRGB(x,y);
	    			result[x][y] = ((rgb & 0x00ff0000) >> 16);
	            }//end for
	        }//end for
    	}else if((color.equals("green"))){
	    	for (int x = 0; x < newWidth; ++x){
	    		for (int y = 0; y < newHeight; ++y){
	    			int rgb=image.getRGB(x,y);
	        	    result[x][y] = ((rgb & 0x0000ff00) >> 8);
	        	}//end for
	    	}//end for
    	}else{
    		for(int x = 0; x < newWidth; ++x){
    			for (int y = 0; y < newHeight; ++y){
    				int rgb=image.getRGB(x,y);
	        	    result[x][y] = (rgb & 0x000000ff);
	        	}//end for
	    	}//end for
    	}//end else
    	return result;
    }//end getArray
	public double[][][] getrescaledY(){return rescaledY;}
	public int getNewWidth(){return newWidth;}
	public int getNewHeight(){return newHeight;}
	public int[] getSplit(){return split;}
	public int getstepSize(){return stepSize;}
	
}//end class
